#include <fstream>
#include <algorithm>
#include <stdexcept>
#include <vector>
#include <deque>
#include <string>
#include <iostream>


#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/foreach.hpp>



#define DEBUG_LEVEL 6
#include <debug_func.h>

#include <open_fstream.h>
#include <set_mark_diff.h>
#include <portable_timer.h>
#include <progress_indicator.h>
#include <stlhelper.h>
#include <merge_unique.h>
#include <ultoa.h>
#include <print_error.h>
#include <get_filename.h>
#include <seq_pairs.h>
#include <crc.h>
#include <digest.h>
#include <erase_non_unique.h>
#include "get_arg.h"


using std::vector;
using std::deque;
using std::ofstream;
using std::ifstream;
using std::ostream;
using std::string;
using std::runtime_error;
using std::cerr;
using std::cin;
using std::cout;
using std::istream;
using std::sort;
using std::streampos;


//
// hash plus field which says whether the hash is from file 1 or 2
//
struct t_digest_dist_pos
	: public t_digest
{
	streampos	file_pos;
	double		distance;
	t_digest_dist_pos(streampos file_pos_ = 0, double d): 
			file_pos(file_pos_), distance(d){}
	bool operator ==(const t_digest_dist_pos& o) const ;
	bool operator <(const t_digest_dist_pos& o) const ;
};

inline bool t_digest_dist_pos::operator ==(const t_digest_dist_pos& o) const
{
	return static_cast<const t_digest&>(*this) == static_cast<const t_digest&>(o);
}
// sort by digest than distance
inline bool t_digest_dist_pos::operator <(const t_digest_dist_pos& o) const
{
	if (static_cast<const t_digest&>(*this) == static_cast<const t_digest&>(o))
	{
		if (distance == o.distance)
		{
			return file_pos < o.file_pos;
		}
		return distance < o.distance;
	}	
	return static_cast<const t_digest&>(*this) < static_cast<const t_digest&>(o);
}
struct t_sort_by_digest_file_pos
{
	bool operator()(const t_digest_dist_pos& a, const t_digest_dist_pos& b)
	{
		if (static_cast<const t_digest&>(a) == static_cast<const t_digest&>(b))
			return a.file_pos < b.file_pos;
		return static_cast<const t_digest&>(a) < static_cast<const t_digest&>(b);
	}
};

struct t_sort_by_file_pos
{
	bool operator()(const t_digest_dist_pos& a, const t_digest_dist_pos& b)
	{
		return a.file_pos < b.file_pos;
	}
};


//8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888

//		Main

//8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888



//________________________________________________________________________________________

//		open_output_file

//________________________________________________________________________________________
bool open_output_file(ofstream& os, const string& file_name)
{
	if (!file_name.length())   
		return false;
	namespace fs = boost::filesystem;
	if (fs::exists(file_name) && fs::is_directory(file_name))
		throw std::runtime_error(file_name + " already exists and is a directory not a file.");
	os.open(file_name.c_str());
	if (!os)
		throw std::runtime_error("Could not open " + file_name);
	return true;
}



//________________________________________________________________________________________

//		parse_blast_pairs_from_file

//________________________________________________________________________________________
struct do_parse_pairs
{
	deque<t_digest_dist_pos>&	id_digests;
	bool						keep_self_self;
	std::string					comment_letters;
	streampos   				curr_pos;
	do_parse_pairs(	deque<t_digest_dist_pos>&	id_digests_,
					bool						keep_self_self_,
					const std::string&			comment_letters_):
					id_digests(id_digests_),
					keep_self_self(keep_self_self_), 
					comment_letters(comment_letters_),
					curr_pos(0){}

	void operator()(istream& is, unsigned)
	{
		if (!curr_pos)
		{
			curr_pos = is.tellg();
			// up by one because file position is just *before* the new line
			// duh!
			if (curr_pos)
				curr_pos += 1U;
		}
		
		// get data for one protein pair
		string name1, name2;

		// comment if false
		string comment_line;
		if (!seq_pairs::parse_ids(is, name1, name2, comment_line, comment_letters,
								  seq_pairs::ePARSE_IDS_ONLY))
		{						  
			// comment line includes new line
			curr_pos += comment_line.length();
			return;
		}

		// normalise ids
		if (name1 > name2)
			swap(name1, name2);
		string normalized_names = name1+"\t"+name2;

		
		// get distance
		string str;
		is >> str;
		if (!is)
			throw runtime_error("The pairs distances data is not in the correct format");
		char* end;
		double dist = strtod(str.c_str(), &end);
		if (end != str.c_str() + str.length())
			throw runtime_error("The pairs distances data is not in the correct format");

		// discard self-self comparisons
		if (keep_self_self || name1 != name2)
		{
			// calculate and save hash
			id_digests.push_back(t_digest_dist_pos(curr_pos, dist));
			crc_hash(normalized_names, id_digests.back());
		}

		// add two for second tab and new line
		curr_pos += normalized_names.length() + str.length() + 2;
	}
};




//________________________________________________________________________________________

//		print_matching

//________________________________________________________________________________________
void print_closest_seq_pairs(deque<t_digest_dist_pos>&	id_digests,
							  ifstream& ifs, const string& file_name,
							t_program_args& args)
{
	// go back to start
	ifs.clear();
	ifs.seekg(0, ifstream::beg);

	t_progress_indicator dots(args.VERBOSE(), id_digests.size() / 100 + 1);
	dots.set_limit(id_digests.size());
	dots.use_timer();

	//
	// Go to each file position and read
	//
	streampos curr_pos = 0;
	string line;
	try
	{
		BOOST_FOREACH(t_digest_dist_pos& digest, id_digests) 
		{
			if (curr_pos != digest.file_pos)
			{
				ifs.seekg(digest.file_pos, std::ios::beg);
				curr_pos = digest.file_pos;
			}
			if (!getline(ifs, line))
				throw std::runtime_error("Could not read from position #" + 
										 ultoa(digest.file_pos) + " of " + file_name);
			if (line.empty())
				throw std::runtime_error("Unexpected empty line at position #" + 
										 ultoa(digest.file_pos) + " of " + file_name);
			curr_pos += line.length() + 1;
			cout << line << "\n";
			++dots;
		}
	}
	catch(std::runtime_error& e)
	{
		throw std::runtime_error(string(e.what()) + 
								 " at position #" + ultoa(curr_pos) + " of " + file_name +
								 "\n[" + line + "]");
	}
}



//________________________________________________________________________________________

//		main

//________________________________________________________________________________________
int main(int argc, char* argv[])
{
	t_program_args args;

	try
	{

	debug_func_cerr(5);

	// don't use C streams
	std::ios::sync_with_stdio(false);


	// get command line arguments
	if (!process_command_line_options(argc, argv, args))
		return 1;


	//
	//	Get IDs from input file1
	//
	string short_file_name = get_lastdir_filename(args.file_input);
	args.VERBOSE() << "\tParsing ids from [" << short_file_name << "]...\n";
	deque<t_digest_dist_pos>	id_digests;
	do_parse_pairs parse_pairs(id_digests, args.self_self, args.comment_letters);
	args.VERBOSE() << "\tReading in paired distance data...\n";
	seq_pairs::read_data(parse_pairs, args.file_input, args.VERBOSE());

	args.VERBOSE() << "\t" << id_digests.size() << "\tsequence pairs read\n";
	args.SUMMARY() << id_digests.size() << "\tsequence pairs read\n";
	if (!id_digests.size())
	{
		std_print_warning("No pairs distances data supplied from STDIN.");
		return 0;
	}

	//
	// sort by id then distance and remove duplicates with larger distances
	//
	sort(id_digests.begin(), id_digests.end());
	erase_non_unique(id_digests);
	args.VERBOSE() << "\t" << id_digests.size() << "\tunique sequence pairs\n";
	args.SUMMARY() << id_digests.size() << "\tunique sequence pairs\n";

	//
	// sort by filepos and print out
	//
	sort(id_digests.begin(), id_digests.end(), t_sort_by_file_pos());
	ifstream ifs (args.file_input.c_str());
	print_closest_seq_pairs(id_digests, ifs, args.file_input, args);

	args.VERBOSE() << "\tAll finished.\n";

	}
	catch (std::exception& e)
	{
		std_print_error(args.CERR(), e.what());
		return 1;
	}

	return 0;
}

